package com.erics.shortlink.project.service.Impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.erics.shortlink.project.common.convention.exception.ClientException;
import com.erics.shortlink.project.common.convention.exception.ServiceException;
import com.erics.shortlink.project.common.enums.VailDateTypeEnum;
import com.erics.shortlink.project.dao.entity.ShortLinkDO;
import com.erics.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.erics.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.erics.shortlink.project.dao.mapper.ShortLinkMapper;
import com.erics.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.erics.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.erics.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.erics.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.erics.shortlink.project.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.erics.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.erics.shortlink.project.service.ShortLinkService;
import com.erics.shortlink.project.util.HashUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import static com.erics.shortlink.project.common.constants.RedisKeyConstant.GOTO_SHORT_LINK_KEY;
import static com.erics.shortlink.project.common.constants.RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY;


/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    private final ShortLinkMapper shortLinkMapper;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;

    private final RedissonClient redissonClient;


    /**
     * 创建短链接
     *
     * @param requestParam
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        String shortLinkSuffix = generateSuffix(requestParam);
        String fullShortUrl = StrBuilder.create(requestParam.getDomain())
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(requestParam.getDomain())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortUrl)
                .build();
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {
            //主要是为了防止多线程并发情况下的错误，有多个线程可能会拿到相同的不存在的URI然后返回，接着去插入数据库
            LambdaQueryWrapper<ShortLinkDO> eq = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
            ShortLinkDO hasShortLinkDO = baseMapper.selectOne(eq);
            if (hasShortLinkDO != null) {
                throw new ServiceException(String.format("短链接：%s 生成重复", fullShortUrl));
            }
        }
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    /**
     * 获取短链接列表
     *
     * @param requestParam
     * @return
     */
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        LambdaQueryWrapper<ShortLinkDO> eq = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getDelFlag, 0)
                .orderByDesc(ShortLinkDO::getCreateTime);
        IPage<ShortLinkDO> resultPage = baseMapper.selectPage(requestParam, eq);
        return resultPage.convert(each -> {
                    ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
                    result.setDomain("http://" + result.getDomain());
                    return result;
                }
        );
    }

    /**
     * 获取短链接分组数量
     *
     * @param requestParam
     * @return
     */
    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                .select("gid as gid, count(*) as shortLinkCount")
                .in("gid", requestParam)
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .groupBy("gid");
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    /**
     * 修改短链接
     *
     * @param requestParam
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        // 构建查询条件，根据 gid、fullShortUrl、delFlag 和 enableStatus 查询短链接记录
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);

        // 查询是否存在对应的短链接记录
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);

        // 如果记录不存在，抛出客户端异常
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }

        // 构建更新的短链接对象，保留原有部分字段，并更新用户传入的新字段值
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(hasShortLinkDO.getDomain()) // 域名
                .shortUri(hasShortLinkDO.getShortUri()) // 短链接后缀
                .clickNum(hasShortLinkDO.getClickNum()) // 保留点击量
                .favicon(hasShortLinkDO.getFavicon()) // 保留网站标识
                .createdType(hasShortLinkDO.getCreatedType()) // 创建类型不可更改
                .gid(requestParam.getGid()) // 分组标识更新
                .originUrl(requestParam.getOriginUrl()) // 原始链接更新
                .describe(requestParam.getDescribe()) // 描述更新
                .validDateType(requestParam.getValidDateType()) // 有效期类型更新
                .validDate(requestParam.getValidDate()) // 有效期更新
                .build();

        // 判断是否为同一个分组
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            // 同一个分组，执行更新操作

            // 构建更新条件
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null); // 如果是永久有效，则将 validDate 设为 null

            // 执行更新操作
            baseMapper.update(shortLinkDO, updateWrapper);
        } else {
            // 不同分组，执行先删除后插入操作

            // 构建删除条件
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);

            // 删除旧记录
            baseMapper.delete(updateWrapper);

            // 插入新记录
            baseMapper.insert(shortLinkDO);
        }
    }


    /**
     * 短链接跳转：根据短链接后缀跳转到原始链接
     *
     * @param shortUri 短链接后缀
     * @param request  HTTP 请求
     * @param response HTTP 响应
     */
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 获取完整的短链接 URL
        String fullShortUrl = request.getServerName() + "/" + shortUri;

        // 从 Redis 中获取原始链接
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        if (StrUtil.isNotBlank(originalLink)) {
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }

        // 获取并加锁，防止缓存击穿
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        lock.lock();
        try {
            // 再次检查缓存，避免在获取锁期间其他线程已加载数据
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originalLink)) {
                ((HttpServletResponse) response).sendRedirect(originalLink);
                return;
            }

            // 查询短链接跳转记录
            LambdaQueryWrapper<ShortLinkGotoDO> gotoWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(gotoWrapper);
            if (shortLinkGotoDO == null) {
                // 如果没有对应的跳转记录，建议记录日志或进行封控处理
                log.warn("未找到对应的短链接跳转记录: {}", fullShortUrl);
                return;
            }

            // 查询短链接详细信息
            LambdaQueryWrapper<ShortLinkDO> linkWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(linkWrapper);

            if (shortLinkDO != null) {
                // 将原始链接缓存到 Redis，并进行跳转
                stringRedisTemplate.opsForValue().set(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl), shortLinkDO.getOriginUrl());
                ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
            } else {
                log.warn("未找到有效的短链接信息: {}", fullShortUrl);
            }
        } finally {
            lock.unlock(); // 释放锁
        }
    }


    /**
     * 生成短链接
     *
     * @param requestParam
     * @return
     */
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            String originUrl = requestParam.getOriginUrl();
            originUrl += UUID.randomUUID().toString();
            shorUri = HashUtil.hashToBase62(originUrl);
            if (!shortUriCreateCachePenetrationBloomFilter.contains(requestParam.getDomain() + "/" + shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

}
